SubAgents
SubAgents are child agents that can be delegated complex tasks. Unlike AIFunctions (single operations) or Skills (guided workflows), SubAgents have their own reasoning capabilities and can autonomously work through multi-step problems.
What Are SubAgents?
A SubAgent is a fully-configured agent exposed as a tool:
Parent Agent SubAgent
┌─────────────────┐ ┌─────────────────┐
│ "Review this PR"│ ────────► │ Code Reviewer │
│ │ │ │
│ │ │ • Reads files │
│ │ │ • Analyzes code │
│ │ ◄──────── │ • Returns report│
└─────────────────┘ └─────────────────┘
delegates works autonomouslyWhen to use SubAgents:
- Tasks requiring autonomous multi-step reasoning
- Specialized domains (code review, research, analysis)
- Isolating complex workflows from the parent agent
Basic Usage
Mark a method with [SubAgent] and return a SubAgent object:
public class DelegationTool
{
[SubAgent]
public SubAgent CodeReviewer()
{
var config = new AgentConfig
{
Name = "Code Reviewer",
SystemInstructions = @"
You are an expert code reviewer. When given code to review:
1. Check for bugs and logic errors
2. Evaluate code style and readability
3. Suggest improvements
4. Rate overall quality (1-10)"
};
return SubAgentFactory.Create(
name: "Code Review",
description: "Reviews code for quality, bugs, and best practices",
agentConfig: config
);
}
}SubAgentFactory Methods
Create() - Stateless
Each invocation starts fresh with a new conversation thread:
public static SubAgent Create(
string name,
string description,
AgentConfig agentConfig,
params Type[] toolTypes // Optional tools for the SubAgent
)[SubAgent]
public SubAgent Researcher()
{
return SubAgentFactory.Create(
name: "Research",
description: "Researches topics thoroughly",
agentConfig: new AgentConfig { SystemInstructions = "..." }
);
}Use when: Each task is independent; no context needs to persist.
CreateStateful() - Shared Session
Maintains conversation context across multiple invocations. All calls reuse the same session, so the sub-agent accumulates memory across turns.
public static SubAgent CreateStateful(
string name,
string description,
AgentConfig agentConfig,
params Type[] toolTypes
)[SubAgent]
public SubAgent ProjectAssistant()
{
return SubAgentFactory.CreateStateful(
name: "Project Assistant",
description: "Helps with ongoing project tasks, remembers context",
agentConfig: new AgentConfig { SystemInstructions = "..." }
);
}To pin the shared session to a specific branch other than "main", set SharedBranchId after creation:
[SubAgent]
public SubAgent ReviewAssistant()
{
var subAgent = SubAgentFactory.CreateStateful(
name: "Review Assistant",
description: "Tracks review feedback across the session",
agentConfig: new AgentConfig { SystemInstructions = "..." }
);
subAgent.SharedBranchId = "review-thread"; // uses this branch instead of "main"
return subAgent;
}Use when: The SubAgent needs to remember previous interactions (e.g., ongoing project work).
CreatePerSession() - Parent context inheritance
The sub-agent inherits the parent agent's current session and branch at invocation time. It sees the full conversation history up to that point, but runs in its own isolated turn and does not write back to the parent branch.
Falls back to stateless if invoked outside of a parent session context.
public static SubAgent CreatePerSession(
string name,
string description,
AgentConfig agentConfig,
params Type[] toolTypes
)[SubAgent]
public SubAgent Summarizer()
{
return SubAgentFactory.CreatePerSession(
name: "Summarize Conversation",
description: "Summarizes everything said so far in the conversation",
agentConfig: new AgentConfig { SystemInstructions = "..." }
);
}Use when: The sub-agent needs context from the parent conversation to do its job (e.g., summarization, analysis of what was said, follow-up research based on the thread so far).
Session Modes Explained
| Mode | Session Behaviour | Memory | Use Case |
|---|---|---|---|
Stateless | New isolated session per call | None | Independent tasks |
SharedSession | Reuses the same session across calls | Persistent | Ongoing multi-turn collaboration |
PerSession | Inherits parent's session as read-only context | Parent's history (read) | Summarization, analysis, context-aware tasks |
Example: All Three Modes
// Stateless: Each review is a fresh call — no context carried over
[SubAgent]
public SubAgent IndependentReviewer()
{
return SubAgentFactory.Create(
name: "Review Code",
description: "Reviews a single piece of code",
agentConfig: reviewerConfig
);
}
// SharedSession: Remembers previous reviews across all invocations
[SubAgent]
public SubAgent ProjectReviewer()
{
return SubAgentFactory.CreateStateful(
name: "Project Reviewer",
description: "Reviews code with project context, remembers past reviews",
agentConfig: reviewerConfig
);
}
// PerSession: Sees the parent's conversation history; useful for summarizing what was discussed
[SubAgent]
public SubAgent ConversationSummarizer()
{
return SubAgentFactory.CreatePerSession(
name: "Summarize",
description: "Summarizes everything discussed so far in this conversation",
agentConfig: summarizerConfig
);
}SubAgents with Tools
SubAgents can have their own tools:
[SubAgent]
public SubAgent FileAnalyzer()
{
var config = new AgentConfig
{
Name = "File Analyzer",
SystemInstructions = "Analyze files and provide insights..."
};
return SubAgentFactory.Create(
name: "Analyze Files",
description: "Analyzes files for patterns, issues, and insights",
agentConfig: config,
typeof(FileSystemTool), // SubAgent can read files
typeof(SearchTool) // SubAgent can search
);
}The SubAgent only has access to the tools you explicitly provide.
Provider Inheritance
By default, SubAgents inherit the parent agent's provider (chat client). If you don't specify a Provider in the SubAgent's AgentConfig, it automatically uses the same LLM provider as the parent.
[SubAgent]
public SubAgent SimpleReviewer()
{
// No Provider specified = inherits parent's provider
var config = new AgentConfig
{
Name = "Simple Reviewer",
SystemInstructions = "Review code for issues..."
};
return SubAgentFactory.Create(
name: "Review",
description: "Quick code review",
agentConfig: config // Uses parent's provider automatically
);
}This is convenient for most cases—your SubAgents use the same model as the parent without extra configuration.
Overriding the Provider
To use a different provider (e.g., a cheaper model for simple tasks, or a specialized model), explicitly specify Provider in the config:
[SubAgent]
public SubAgent SpecializedAnalyzer()
{
var config = new AgentConfig
{
Name = "Specialized Analyzer",
SystemInstructions = "...",
Provider = new ProviderConfig
{
ProviderKey = "openai",
ModelName = "gpt-4o-mini", // Use cheaper model for this task
ApiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
}
};
return SubAgentFactory.Create(
name: "Specialized Analysis",
description: "Uses GPT-4o-mini for cost-effective analysis",
agentConfig: config
);
}Common patterns:
- Use a cheaper/faster model for simple SubAgent tasks
- Use a specialized model (e.g., code-focused) for domain-specific work
- Use a different provider entirely (e.g., parent uses Anthropic, SubAgent uses OpenAI)
Permission Behavior
SubAgents always require permission by default. This is because they can perform multiple autonomous actions.
Unlike AIFunctions where [RequiresPermission] is opt-in, SubAgents are inherently permission-required:
// No [RequiresPermission] needed - it's implicit
[SubAgent]
public SubAgent DangerousOperations()
{
return SubAgentFactory.Create(
name: "System Admin",
description: "Performs system administration tasks",
agentConfig: adminConfig,
typeof(SystemTool)
);
}
// User will ALWAYS be prompted before this SubAgent runsEvent Bubbling
All events from a SubAgent flow into the parent's RunAsync stream — there is no separate stream to consume. Every AgentEvent carries an ExecutionContext property you can use to identify which agent emitted it:
AgentName— the SubAgent's nameDepth— nesting level (0= parent agent,1= SubAgent,2= sub-sub-agent)AgentChain— full hierarchy, e.g.["ParentAgent", "Code Reviewer"]
await foreach (var evt in agent.RunAsync("Review this PR", sessionId: sessionId))
{
if (evt is AgentEvent agentEvt)
{
var depth = agentEvt.ExecutionContext?.Depth ?? 0;
var name = agentEvt.ExecutionContext?.AgentName ?? "unknown";
switch (agentEvt)
{
case TextDeltaEvent delta when depth == 0:
// Parent agent speaking
Console.Write(delta.Text);
break;
case TextDeltaEvent delta when depth == 1:
// SubAgent working — show indented
Console.Write($" [{name}] {delta.Text}");
break;
case MessageTurnFinishedEvent finished when depth == 1:
Console.WriteLine($"\n [{name}] done");
break;
}
}
}
---
## Typed Metadata
For compile-time validation, use the generic attribute:
```csharp
public class DelegationMetadata : IToolMetadata
{
public bool HasSpecializedAgents { get; set; }
// Implementation...
}
public class DelegationTool
{
[SubAgent<DelegationMetadata>]
public SubAgent SpecialAgent()
{
return SubAgentFactory.Create(
name: "Special Agent",
description: "Specialized task handler",
agentConfig: specialConfig
);
}
}→ See 02.1.4 Tool Metadata.md for details.
Conditional SubAgents
Show or hide SubAgents based on runtime conditions:
[SubAgent]
[ConditionalSubAgent("HasSpecializedAgents")]
public SubAgent AdvancedAnalyzer()
{
// Only visible when metadata.HasSpecializedAgents is true
return SubAgentFactory.Create(
name: "Advanced Analysis",
description: "Advanced analysis capabilities",
agentConfig: advancedConfig
);
}→ See 02.1.4 Tool Metadata.md for conditional registration details.
SubAgents vs Skills vs AIFunctions
| Aspect | AIFunction | Skill | SubAgent |
|---|---|---|---|
| Complexity | Single operation | Multi-step workflow | Autonomous reasoning |
| Control | Direct | Guided by instructions | Delegated |
| Memory | None | None | Optional (stateful) |
| Tools | Parent's | Parent's (referenced) | Own set |
| Provider | Parent's | Parent's | Configurable |
| Permission | Opt-in | Opt-in | Always required |
Decision guide:
- AIFunction: "Call this function with these parameters"
- Skill: "Follow these steps using these functions"
- SubAgent: "Figure out how to accomplish this goal"
Best Practices
Give SubAgents clear, focused purposes: A SubAgent should excel at one domain.
Write detailed system instructions: SubAgents rely heavily on their instructions for autonomous work.
Provide only necessary tools: Don't give SubAgents access to tools they don't need.
Use stateful mode sparingly: Shared threads consume more resources; use only when context is genuinely needed.
Consider provider costs: SubAgents make their own LLM calls; expensive models add up.
// Good: Focused, well-instructed, minimal tools
[SubAgent]
public SubAgent SecurityAuditor()
{
var config = new AgentConfig
{
Name = "Security Auditor",
SystemInstructions = @"
You are a security expert. When auditing code:
1. Check for OWASP Top 10 vulnerabilities
2. Identify authentication/authorization issues
3. Look for data exposure risks
4. Provide severity ratings (Critical/High/Medium/Low)
5. Suggest specific fixes for each issue
Be thorough but focused. Don't report style issues."
};
return SubAgentFactory.Create(
name: "Security Audit",
description: "Audits code for security vulnerabilities",
agentConfig: config,
typeof(FileSystemTool) // Only needs to read files
);
}
// Bad: Vague purpose, overpowered
[SubAgent]
public SubAgent DoAnything()
{
return SubAgentFactory.Create(
name: "Helper",
description: "Helps with stuff",
agentConfig: new AgentConfig { SystemInstructions = "Help the user" },
typeof(FileSystemTool),
typeof(DatabaseTool),
typeof(NetworkTool),
typeof(SystemTool) // Way too many capabilities
);
}Next Steps
- 02.1.4 Tool Metadata.md - Dynamic descriptions and conditionals
- 02.1.5 Context Engineering.md - Context management